/***********************************************************************************************************************
*  OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
*  See also https://openstudio.net/license
***********************************************************************************************************************/

#include "ComponentWatcher.hpp"
#include "ComponentWatcher_Impl.hpp"

#include "Model.hpp"
#include "Model_Impl.hpp"

#include "ModelObject.hpp"
#include "ModelObject_Impl.hpp"

#include "ComponentData_Impl.hpp"
#include "../nano/nano_signal_slot.hpp"

#include "../utilities/core/Assert.hpp"
#include "../utilities/core/Compare.hpp"

namespace openstudio {
namespace model {

  namespace detail {

    ComponentWatcher_Impl::ComponentWatcher_Impl(ComponentData& componentData) : m_componentData(componentData) {
      // get componentObjects and connect signals
      ModelObject_ImplPtr implPtr;
      if (componentData.numComponentObjects() < 1) {
        LOG_AND_THROW("Invalid ComponentData object--no objects listed in contents.");
      }
      for (unsigned i = 0, n = componentData.numComponentObjects(); i < n; ++i) {
        // get object
        OptionalModelObject omo = componentData.getComponentObject(i);
        if (!omo) {
          LOG_AND_THROW("Invalid ComponentData object--blank entry in contents list.");
        }
        ModelObject object = *omo;
        m_componentObjects.push_back(object);
        // connect to object's signals
        implPtr = object.getImpl<ModelObject_Impl>();
        // data change
        implPtr.get()->ModelObject_Impl::onDataChange.connect<ComponentWatcher_Impl, &ComponentWatcher_Impl::dataChange>(this);
        // name change
        implPtr.get()->ModelObject_Impl::onNameChange.connect<ComponentWatcher_Impl, &ComponentWatcher_Impl::nameChange>(this);
        // relationship change
        implPtr.get()->ModelObject_Impl::onRelationshipChange.connect<ComponentWatcher_Impl, &ComponentWatcher_Impl::relationshipChange>(this);
        // remove
        implPtr.get()->ModelObject_Impl::onRemoveFromWorkspace.connect<ComponentWatcher_Impl, &ComponentWatcher_Impl::objectRemove>(this);
      }
      // connect signals for ComponentData, ComponentDataTags, and ComponentDataAttributes
      implPtr = componentData.getImpl<ModelObject_Impl>();
      // component data change
      implPtr.get()->ModelObject_Impl::onDataChange.connect<ComponentWatcher_Impl, &ComponentWatcher_Impl::componentDataChange>(this);
      // component data remove
      implPtr.get()->ModelObject_Impl::onRemoveFromWorkspace.connect<ComponentWatcher_Impl, &ComponentWatcher_Impl::objectRemove>(this);
      // connect to addWorkspaceObject signal
      std::shared_ptr<Model_Impl> modelImplPtr = m_componentData.model().getImpl<Model_Impl>();
      modelImplPtr.get()->Model_Impl::addWorkspaceObject.connect<ComponentWatcher_Impl, &ComponentWatcher_Impl::objectAdd>(this);
    }

    ComponentWatcher ComponentWatcher_Impl::componentWatcher() const {
      ComponentWatcher result(std::const_pointer_cast<ComponentWatcher_Impl>(shared_from_this()));
      return result;
    }

    ComponentData ComponentWatcher_Impl::componentData() const {
      return m_componentData;
    }

    void ComponentWatcher_Impl::dataChange() {
      mf_changeComponentVersion();
    }

    void ComponentWatcher_Impl::nameChange() {
      mf_changeComponentVersion();
    }

    void ComponentWatcher_Impl::componentDataChange() {
      mf_changeComponentVersion();
      return;
    }

    void ComponentWatcher_Impl::relationshipChange(int /*index*/, Handle newHandle, Handle oldHandle) {
      OS_ASSERT(newHandle != oldHandle);
      // if oldHandle is in the Component nullify the component
      HandleVector hs = getHandles<ModelObject>(m_componentObjects);
      if (std::find(hs.begin(), hs.end(), oldHandle) != hs.end()) {
        mf_removeComponent();
        return;
      }
      // otherwise, change version
      mf_changeComponentVersion();
      return;
    }

    void ComponentWatcher_Impl::objectRemove(const Handle& handleOfRemovedObject) {

      // no ComponentData means no Component to watch
      if (handleOfRemovedObject == m_componentData.handle()) {
        this->obsolete.nano_emit(componentWatcher());
        return;
      }

      // if removedObject is the primary componentObject, nullify the component.
      OS_ASSERT(!m_componentObjects.empty());
      if (handleOfRemovedObject == m_componentObjects[0].handle()) {
        mf_removeComponent();
        return;
      }

      // if removedObject is a componentObject, remove from the vector and refresh
      // component contents
      auto it = std::find_if(m_componentObjects.begin(), m_componentObjects.end(), [&handleOfRemovedObject](const ModelObject& mo) {
        return handleEquals<ModelObject, Handle>(mo, handleOfRemovedObject);
      });
      if (it != m_componentObjects.end()) {
        OS_ASSERT(it != m_componentObjects.begin());
        m_componentObjects.erase(it);
        mf_refreshComponentContents(false);
        return;
      }
    }

    void ComponentWatcher_Impl::objectAdd(const WorkspaceObject& addedObject, const openstudio::IddObjectType& /*type*/,
                                          const openstudio::UUID& /*uuid*/) {
      /*IddObjectType type =*/addedObject.iddObject().type();
      return;
    }

    void ComponentWatcher_Impl::mf_changeComponentVersion() {
      // disconnect componentDataChange slot to avoid endless loop
      std::shared_ptr<ModelObject_Impl> implPtr = m_componentData.getImpl<ModelObject_Impl>();
      implPtr.get()->onDataChange.disconnect<ComponentWatcher_Impl, &ComponentWatcher_Impl::componentDataChange>(this);

      // change version UUID
      m_componentData.createVersionUUID();

      // reconnect componentDataChange
      implPtr.get()->ModelObject_Impl::onDataChange.connect<ComponentWatcher_Impl, &ComponentWatcher_Impl::componentDataChange>(this);
    }

    void ComponentWatcher_Impl::mf_refreshComponentContents(bool /*logWarnings*/) {
      // disconnect componentDataChange slot to avoid endless loop
      std::shared_ptr<ModelObject_Impl> implPtr = m_componentData.getImpl<ModelObject_Impl>();
      implPtr.get()->onDataChange.disconnect<ComponentWatcher_Impl, &ComponentWatcher_Impl::componentDataChange>(this);

      // \todo logWarnings
      m_componentData.clearExtensibleGroups();
      for (const ModelObject& object : m_componentObjects) {
        m_componentData.getImpl<ComponentData_Impl>()->registerObject(object);
      }

      // reconnect componentDataChange
      implPtr.get()->ModelObject_Impl::onDataChange.connect<ComponentWatcher_Impl, &ComponentWatcher_Impl::componentDataChange>(this);
    }

    void ComponentWatcher_Impl::mf_removeComponent() {
      m_componentData.remove();  // triggers emit through slot objectRemove
    }

  }  // namespace detail

  ComponentWatcher::ComponentWatcher(ComponentData& componentData) : m_impl(new detail::ComponentWatcher_Impl(componentData)) {}

  ComponentData ComponentWatcher::componentData() const {
    return m_impl->componentData();
  }

  bool ComponentWatcher::operator==(const ComponentWatcher& other) {
    return m_impl == other.m_impl;
  }

  bool ComponentWatcher::operator!=(const ComponentWatcher& other) {
    return (!operator==(other));
  }

  /// @cond
  std::shared_ptr<detail::ComponentWatcher_Impl> ComponentWatcher::getImpl() const {
    return m_impl;
  }

  ComponentWatcher::ComponentWatcher(std::shared_ptr<detail::ComponentWatcher_Impl> impl) : m_impl(std::move(impl)) {}
  /// @endcond

}  // namespace model
}  // namespace openstudio
